Skip to content

Conversation

andrewyuq
Copy link
Contributor

  1. Since the shift from displaying one trigger at a time to displaying multiple triggers at a time, use SessionContext as the disposable for the session states.
  2. Introduce the concept of "display session" which can show multiple triggers at a time and sessionContext is to hold states for that session.
  3. ongoingRequests will be a map of <jobId, InvocationContext> where InvocationContext is the state of each trigger.
  4. Managed these states globally by dynamically adding Trigger states into ongoingRequests and whenever the user made a decision(accept or reject), conclude decisions for all the ongoing triggers and clear ongoingRequest, so that a new display session can start later.

Types of changes

  • Bug fix (non-breaking change which fixes an issue)
  • New feature (non-breaking change which adds functionality)

Description

Checklist

  • My code follows the code style of this project
  • I have added tests to cover my changes
  • A short description of the change has been added to the CHANGELOG if the change is customer-facing in the IDE.
  • I have added metrics for my changes (if required)

License

I confirm that my contribution is made under the terms of the Apache 2.0 license.

1. Since the shift from displaying one trigger at a time
to displaying multiple triggers at a time, use SessionContext
as the disposable for the session states.
2. Introduce the concept of "display session" which can show
multiple triggers at a time and sessionContext is to hold states
for that session.
3. ongoingRequests will be a map of <jobId, InvocationContext>
where InvocationContext is the state of each trigger.
4. Managed these states globally by dynamically adding Trigger states
into ongoingRequests and whenever the user made a decision(accept or
reject), conclude decisions for all the ongoing triggers and clear
ongoingRequest, so that a new display session can start later.
@andrewyuq andrewyuq requested a review from rli September 25, 2024 22:32
@andrewyuq andrewyuq requested a review from a team as a code owner September 25, 2024 22:32
private var refreshFailure: Int = 0
private val ongoingRequests = mutableMapOf<Int, InvocationContext?>()
val ongoingRequestsContext = mutableMapOf<Int, RequestContext>()
private var jobId = 0
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

can this overflow?

Copy link
Contributor Author

@andrewyuq andrewyuq Sep 26, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We have a monthly trigger limit which is 25000, so jobId will increment 25000 each month if they don't restart IDE.

Comment on lines +99 to +100
private val ongoingRequests = mutableMapOf<Int, InvocationContext?>()
val ongoingRequestsContext = mutableMapOf<Int, RequestContext>()
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

not sure if this is the best pattern if the exact jobId is only relevant for "which is the last request", which we can determine in other ways

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

jobId is only to separate different triggers and is a local identifier only, (same as sessionId except we know the jobId as soon as a trigger happens before receiving the response)

return
}
val caretContext = requestContext.fileContextInfo.caretContext
ongoingRequestsContext.forEach { (k, v) ->
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

for example, this is guaranteed to iterate in-order from first request inserted in the map to last

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

yea, the purpose is not just to identify which the last request is -- but more to separate states of different triggers.

Later on when we send UserTriggerDecisions we will need to send one for each jobId(trigger), having a jobId here and logging it keeps track of what happened E2E for this specific trigger.

// It's possible and ok that coroutine will keep running until the next time we check it's state.
// As long as we don't show to the user extra info we are good.
val coroutineScope = disposableCoroutineScope(popup)
val coroutineScope = projectCoroutineScope(requestContext.project)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

CodeWhispererService has a service-level scope that should be used instead

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

fixed


data class RecommendationContext(
val details: List<DetailContext>,
val details: MutableList<DetailContext>,
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

not seeing why need mutable

Copy link
Contributor Author

@andrewyuq andrewyuq Sep 26, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Now each trigger will only have one state InvocationContext and is stored in ongoingRequests, when subsequent suggestions sent back to client, they will be added to this mutable details, and other parts of InvocationContext remains the same.

private fun updateStates(
        states: InvocationContext,
        response: GenerateCompletionsResponse
    ): InvocationContext {
        val recommendationContext = states.recommendationContext
        val newDetailContexts = CodeWhispererRecommendationManager.getInstance().buildDetailContext(
            states.requestContext,
            recommendationContext.userInputSinceInvocation,
            response.completions(),
            response.responseMetadata().requestId()
        )

        recommendationContext.details.addAll(newDetailContexts)
        return states
    }

otherwise we will have to replace InvocationContext which I feel is a bit unnecessary.

Comment on lines +147 to +153
if (hasAccepted) {
popup?.closeOk(null)
} else {
popup?.cancel()
}
popup?.let { Disposer.dispose(it) }
popup = null
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

popup manager and sessioncontext are so tied together that it feels like it should just be handled together

Copy link
Contributor Author

@andrewyuq andrewyuq Sep 26, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

popup now becomes a state of a session (1 session 1 popup) so I put it as a field of SessionContext, and then I let disposing SessionContext handles everything so I don't have to call

CWService.getInstance().disposeDisplaySession()
popup.cancel()/close()

separately.

it.closeOk(null)
Disposer.dispose(it)
fun showPopup(sessionContext: SessionContext, force: Boolean = false) {
val p = sessionContext.editor.offsetToXY(sessionContext.popupDisplayOffset)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

popupOffset?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

changed

private fun getValidCount(): Int =
CodeWhispererService.getInstance().getAllSuggestionsPreviewInfo().filter { isValidRecommendation(it) }.size

private fun getValidSelectedIndex(selectedIndex: Int): Int {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

feels like this could be cleaner but i see that it's the same as the old logic

@Will-ShaoHua
Copy link
Contributor

can we add some doc strings for different models

val jobId: Int,
val detail: DetailContext,
val userInput: String,
val typeahead: String,
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

dumb q, what's the difference between these 2 typeaheads

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

previously we have typeahead and typeaheadoriginal for the purpose of accounting leading spaces in the typeahead,
for example, typeaheadoriginal will be System and typeahead will be System, and then we use typeahead to do the suggestion matching logic. This is to preserve the suggestions as much as possible.

Now we trigger more so we have more display oppotunities, I feel like this becomes a bit unnecessary and makes the logic complicated, so remove typeahead original to make it no longer accomodate leading spaces.

private val ongoingRequests = mutableMapOf<Int, InvocationContext?>()
val ongoingRequestsContext = mutableMapOf<Int, RequestContext>()
private var jobId = 0
private var sessionContext: SessionContext? = null
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

not sure do we need this to be thread safe

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Technically it needs, so the current rule is to modify this only in EDT so I believe in various places I have @RequiresEDT annotation including disposeDisplaySession, and initialing SessionContext also is in EDT.

class CodeWhispererService(private val cs: CoroutineScope) : Disposable {
private val codeInsightSettingsFacade = CodeInsightsSettingsFacade()
private var refreshFailure: Int = 0
private val ongoingRequests = mutableMapOf<Int, InvocationContext?>()
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

what's the case when InvocationContext being null

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

before the InvocationContext for the specific jobId is added, it can be null, we did some null check to know if it's first request/response.

Comment on lines +303 to +312
processCodeWhispererUI(
sessionContext,
it,
ongoingRequests[currentJobId],
cs,
currentJobId
)
if (!ongoingRequests.contains(currentJobId)) {
cs.coroutineContext.cancel()
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

andrew could you help me understand why isn't this in the format of

if (ongoingRequests.contains(currentJobId)) {
     processCodeWhispererUi()
}

but execute processCodeWhispererUi and cancel the coroutine later, what's the expected behavior here?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

so intially there's no InvocationContext in ongoingRequests, and then we contruct it in initState in processCodeWhispererUi and put it into the map. If after this function it's still null, then it means state creation failed due to e.g. all discarded suggestions or all empty suggestions.

ongoingRequests[currentJobId],
cs,
currentJobId
)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

why here doesn't need the

if (!ongoingRequests.contains(currentJobId)) {
                                                cs.coroutineContext.cancel()
                                            }

check

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think it still needs, I will add it in the following PR.

sessionId = e.awsErrorDetails().sdkHttpResponse().headers().getOrDefault(KET_SESSION_ID, listOf(requestId))[0]
displayMessage = e.awsErrorDetails().errorMessage() ?: message("codewhisperer.trigger.error.server_side")
} else if (e is software.amazon.awssdk.services.codewhispererruntime.model.CodeWhispererRuntimeException) {
} else if (e is CodeWhispererRuntimeException) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

thanks for the change!

@Will-ShaoHua Will-ShaoHua self-requested a review October 3, 2024 20:27
Comment on lines 120 to 131
val project: Project,
val editor: Editor,
var popup: JBPopup? = null,
var selectedIndex: Int = -1,
val seen: MutableSet<Int> = mutableSetOf(),
val isFirstTimeShowingPopup: Boolean = true,
var isFirstTimeShowingPopup: Boolean = true,
var toBeRemovedHighlighter: RangeHighlighter? = null,
var insertEndOffset: Int = -1
)
var insertEndOffset: Int = -1,
var popupOffset: Int = -1,
val latencyContext: LatencyContext,
var hasAccepted: Boolean = false
) : Disposable {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

some of these can be private / don't need to be initialized in the primary constructor since the initial value is always expected to be the same

@andrewyuq andrewyuq changed the base branch from main to auto-trigger-revamp October 3, 2024 23:18
@andrewyuq andrewyuq merged commit af3c9e6 into aws:auto-trigger-revamp Oct 4, 2024
0 of 8 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants